package authentication;

import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.io.*;

import javax.security.auth.Subject;

import netTools.IPManager;
import netTools.PortManager;

import org.ietf.jgss.*;

import start.StrongBoxServer;
import jaxb.messages.*;

import encryption.EncryptionManager;
import virtualHD.*;

import authorization.FolderMaster;
import authorization.ResourceManager;

/**
 * 
 * @author pasquale
 * 
 * A secure server built from service credentials. It is indeed a thread which takes care of 
 * dealing with clients. Before actual communication, a secure handshaking is performed so that secret
 * keys can be exchanged and communication can be made in a secure way. During handshaking client
 * has to prove his/her identity providing kerberos credentials.
 *
 */
public class SecureServer implements Runnable,FileHandler{ 

	public SecureServer(Socket s,StrongBoxServer server){
		this.socket = s;
		this.subject = server.getSubject();
		this.managers = server.getResourceManagers();
		this.logger = server.getServerLogger();
		
		serverOn = true;

		portManager = server.getPortManager();
		im = server.getIPManager();
	}

	@Override
	public void run(){
		try {
			inStream = new DataInputStream(socket.getInputStream());
			outStream = new DataOutputStream(socket.getOutputStream());


			logger.info("Got connection from client "
					+ socket.getInetAddress());

			//privileged action with service credentials
			Subject.doAsPrivileged(subject, new PrivilegedExceptionAction<Integer>() {

				public Integer run() throws GSSException,IOException {


					GSSManager manager = GSSManager.getInstance();
					context = manager.createContext((GSSCredential)null);

					initSecureContext(context); //init secure context handshake

					return null;
				}
			},null);

			//Encryption Manager initialization
			em = new EncryptionManager(context,new MessageProp(0, true),0);

			//Folder Master initialization
			String username = context.getSrcName().toString().split("@")[0];

			logger.info("Client "+socket.getInetAddress()+" authenticated as "+username);

			FolderMaster fm = new FolderMaster(managers,username,this);

			while(serverOn){
				//listens for messages
				byte[] token = em.readEncMessage(inStream);

				Message rec = new Message(new String(token));
				String op = rec.getType();

				Message response = new Message();

				try{

					//************************************* Operations ****************************************************						
					if(op.equals(FolderMaster.VIEW)){
						Resource res[] = fm.view(rec.getFolder()); //throws a VIRTUAL HDException if fails

						List<Resource> listRes = new ArrayList<Resource>();
						response.setType(Message.OK);
						for(int i=0;i<res.length;i++)
							listRes.add(res[i]);

						response.setResource(listRes);

						em.writeEncMessage(response.toXMLString().getBytes(),outStream);

					}else if(op.equals(FolderMaster.GET)){
						Resource resource = rec.getResource().get(0);
						fm.get(rec.getFolder(), resource);

					}else if(op.equals(FolderMaster.PUT)){
						Resource res = rec.getResource().get(0);
						fm.put(rec.getFolder(), res);	//throws VirtualHDException if fails

					}else if(op.equals(FolderMaster.DEL)){
						Resource res = rec.getResource().get(0);
						fm.del(rec.getFolder(), res);

						response.setType(Message.OK);
						em.writeEncMessage(response.toXMLString().getBytes(),outStream);
					}else if(op.equals(Message.CLOSE)){
						serverOn = false;
						logger.info("Client "+username+"("+socket.getInetAddress()+") closed connection");
					}else if(op.equals(Message.CHANGE_ROLE)){
						Folder fol = rec.getFolder();
						fm.change_role(fol);
						response.setType(Message.OK);
						em.writeEncMessage(response.toXMLString().getBytes(),outStream);		
					}

					//***************************************************************************************************
				}catch(VirtualHDException e){
					response.setType(Message.ERROR);
					response.setCause(e.getMessage());
					em.writeEncMessage(response.toXMLString().getBytes(),outStream);
				}
			}


			//*******************************************************************************************
		} catch (PrivilegedActionException e) {
			e.printStackTrace();
		} catch (GSSException e) {
			logger.warning("Client "+socket.getInetAddress()+" :"+e.getMessage());
			im.addToWarned(socket.getInetAddress()); //adding this ip to warned list
		} catch (SocketTimeoutException e){
			logger.info("Connection with client "+socket.getInetAddress()+" closed due to inactivity");
		} catch (IOException e) {
			logger.warning("Client "+socket.getInetAddress()+" suddently the closed connection");
		} finally{

			try {
				socket.close();
				context.dispose();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (GSSException e) {
				e.printStackTrace();
			}
			
		}
	}

	//************************* file handling methods ********************************************
	@Override
	public void sendFile(InputStream stream) {
		Message portMess = new Message();
		portMess.setType(Message.OK);
		int sendPort = portManager.getPortNumber();
		portMess.setPort(sendPort);
		try {
			ServerSocket ss = new ServerSocket(sendPort);
			ss.setSoTimeout(StrongBoxServer.ACCEPT_TRANSFER_TIMEOUT);
			em.writeEncMessage(portMess.toXMLString().getBytes(),outStream);
			Socket s = ss.accept();
			s.setSoTimeout(StrongBoxServer.TRANSFER_TIMEOUT);
			logger.info("Opened a data connection to IP "+s.getInetAddress()+"/"+s.getPort()+" for SEND");
			try{
				em.secureSend(stream,s.getOutputStream());
			}catch(SocketTimeoutException e1){
				logger.info("Data connection versus "+s.getInetAddress()+" timed out!");
			}catch(IOException e1){
				logger.info("Data connection versus "+s.getInetAddress()+" failed!");
			}

			s.close();
			ss.close();

			logger.info("Data connection versus "+s.getInetAddress()+ " successfully closed");
		} catch (GSSException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			portManager.freePort();
		}

	}

	@Override
	public void receiveFile(OutputStream stream) {
		Message portMess = new Message();
		portMess.setType(Message.OK);
		int recPort = portManager.getPortNumber();
		portMess.setPort(recPort);
		try {
			ServerSocket ss = new ServerSocket(recPort); //waiting on a port for client connection
			ss.setSoTimeout(StrongBoxServer.ACCEPT_TRANSFER_TIMEOUT);

			em.writeEncMessage(portMess.toXMLString().getBytes(),outStream);
			Socket s = ss.accept();
			s.setSoTimeout(StrongBoxServer.TRANSFER_TIMEOUT);

			logger.info("Opened a data connection to IP "+s.getInetAddress()+"/"+s.getPort()+" for SEND");

			try{
				em.secureReceive(s.getInputStream(),stream);
			}catch(GSSException e1){
				logger.warning("Data receiving from "+s.getInetAddress()+" failed for security reasons");
				im.addToWarned(socket.getInetAddress());
			}catch(IOException e1){
				logger.info("Data connection versus "+s.getInetAddress()+" failed!");
			}

			logger.info("Data connection versus "+s.getInetAddress()+ " successfully closed");

			s.close();
			ss.close();

		} catch (GSSException e) {

		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			portManager.freePort();
		}

	}

	//************************** utility methods *************************************************

	/**
	 * Makes the security context handshake
	 * @param context
	 * @return
	 * @throws GSSException
	 * @throws IOException
	 */
	private byte[] initSecureContext(GSSContext context) throws GSSException, IOException{
		byte[] token = null;

		while (!context.isEstablished()) {

			token = new byte[inStream.readInt()];
			inStream.readFully(token);

			token = context.acceptSecContext(token, 0, token.length);

			// Send a token to the peer if one was generated by
			// acceptSecContext
			if (token != null) {
				outStream.writeInt(token.length);
				outStream.write(token);
				outStream.flush();
			}
		}

		System.out.print("Context Established! ");
		System.out.println("Client is " + context.getSrcName());
		System.out.println("Server is " + context.getTargName());
		if (context.getMutualAuthState())
			System.out.println("Mutual authentication took place!");

		return token;

	}


	private Socket socket;
	private DataInputStream inStream;
	private DataOutputStream outStream;
	private Subject subject;
	
	private boolean serverOn;

	private GSSContext context; 
	private EncryptionManager em;

	private ResourceManager[] managers;

	private PortManager portManager;
	private Logger logger;
	private IPManager im;

}
